/*
 * Copyright (C) 2010-2011 Comcast Cable.  All Rights Reserved.
 */
package com.hercules.android.receiver.util.evaluators.impl;

import com.hercules.android.receiver.AndroidReceiver;
import com.hercules.android.receiver.model.objects.XREObject;
import com.hercules.android.receiver.util.evaluators.ExpressionEvaluator;
import com.hercules.android.receiver.util.evaluators.InvalidAndroidReceiverStateException;
import com.hercules.android.receiver.util.evaluators.InvalidExpressionException;
import com.hercules.android.receiver.util.evaluators.InvalidPathException;
import com.hercules.android.receiver.util.evaluators.PathEvaluator;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.logging.Logger;

/**
 * <p>
 * This class is an implementation of ExpressionEvaluator that evaluates Postfix expressions in reverse Polish notation
 * according to the section 4.11 of the XRE Specification. It uses pluggable PathEvaluator instance to evaluate XRE
 * object paths.
 * </p>
 * <p>
 * <em>Sample Code:</em>
 *
 * <pre>
 * // Prepare Android receiver and XRE objects
 * AndroidReceiver receiver = new AndroidReceiver();
 * ApplicationData applicationData = new ApplicationData();
 * receiver.setApplicationData(applicationData);
 * Map&lt;Integer, XREObject&gt; elementsXREIdToObject = new HashMap&lt;Integer, XREObject&gt;();
 * applicationData.setElementsXREIdToObject(elementsXREIdToObject);
 *
 * XREView rootView = new XREView();
 * rootView.setId(2);
 * rootView.setAlpha(0.3);
 *
 * XREView windowView = new XREView();
 * windowView.setId(2001);
 * windowView.setName(&quot;window&quot;);
 * windowView.setAlpha(0.2);
 * windowView.setParent(rootView);
 * rootView.getChildren().add(windowView);
 *
 * XREView titleView = new XREView();
 * titleView.setId(2002);
 * titleView.setName(&quot;title&quot;);
 * titleView.setAlpha(0.1);
 * titleView.setParent(windowView);
 * windowView.getChildren().add(titleView);
 *
 * elementsXREIdToObject.put(2, rootView);
 * elementsXREIdToObject.put(2001, windowView);
 * elementsXREIdToObject.put(2002, titleView);
 *
 * // Create an instance of XREPathEvaluator
 * PathEvaluator pathEvaluator = new XREPathEvaluator(&quot;myLogger&quot;);
 *
 * // Create an instance of XREExpressionEvaluator
 * ExpressionEvaluator expressionEvaluator = new XREExpressionEvaluator(&quot;myLogger&quot;, pathEvaluator);
 *
 * // Evaluate sample expressions
 *
 * double result = expressionEvaluator.evaluateExpression(&quot;{100 200 +}&quot;, 2001, receiver);
 * // result must be 300
 *
 * result = expressionEvaluator.evaluateExpression(&quot;{this.alpha 2 * 0.05 -}&quot;, 2002, receiver);
 * // result must be 0.15
 *
 * result = expressionEvaluator.evaluateExpression(&quot;{$2.alpha $2001.title.alpha - 2 /}&quot;, 2002, receiver);
 * // result must be 0.1
 * </pre>
 *
 * </p>
 * <p>
 * <strong>Thread Safety: </strong> This class is immutable and thread safe when AndroidReceiver instance passed to it
 * is used by the caller in thread safe manner.
 * </p>
 *
 * @author saarixx, sparemax
 * @version 1.0
 */
@SuppressWarnings("boxing")
public class XREExpressionEvaluator implements ExpressionEvaluator {
    /**
     * <p>
     * Represents the class name.
     * </p>
     */
    private static final String CLASS_NAME = XREExpressionEvaluator.class.getName();

    /**
     * <p>
     * Represents the right brace '}'.
     * </p>
     */
    private static final String BRACE_R = "}";

    /**
     * <p>
     * Represents the left brace '{'.
     * </p>
     */
    private static final String BRACE_L = "{";

    /**
     * <p>
     * Represents the '+'.
     * </p>
     */
    private static final char OP_ADD = '+';

    /**
     * <p>
     * Represents the '-'.
     * </p>
     */
    private static final char OP_SUB = '-';

    /**
     * <p>
     * Represents the '*'.
     * </p>
     */
    private static final char OP_MUL = '*';

    /**
     * <p>
     * Represents the '/'.
     * </p>
     */
    private static final char OP_DIV = '/';

    /**
     * <p>
     * Represents the '%'.
     * </p>
     */
    private static final char OP_MOD = '%';

    /**
     * <p>
     * Represents the '^'.
     * </p>
     */
    private static final char OP_POW = '^';

    /**
     * <p>
     * Represents the '+', '-', '*', '/', '%' and '^'.
     * </p>
     */
    private static final String OPERATORS = new String(new char[]{OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_MOD, OP_POW});

    /**
     * <p>
     * Represents the prefix of getter.
     * </p>
     */
    private static final String GETTER_PREFIX = "get";

    /**
     * <p>
     * Represents the ' '.
     * </p>
     * <p>
     * AndroidAssembly2 allowed any count of white spaces
     */
    private static final String SPACE = "\\s+";

    /**
     * <p>
     * The Java logger used by this class for logging errors and debug information.
     * </p>
     * <p>
     * Is null when logging is not required. Is initialized in the constructor and never changed after that. Is used in
     * evaluateExpression().
     * </p>
     */
    private final Logger logger;

    /**
     * <p>
     * The path evaluator.
     * </p>
     * <p>
     * Is initialized in the constructor and never changed after that. Cannot be null.
     * </p>
     */
    private final PathEvaluator pathEvaluator;

    /**
     * <p>
     * Creates an instance of XREExpressionEvaluator.
     * </p>
     *
     * @param pathEvaluator the path evaluator to be used
     * @param loggerName the logger name (<code>null</code> if logging of errors and debug information is not required)
     * @throws IllegalArgumentException if loggerName is empty or pathEvaluator is <code>null</code>.
     */
    public XREExpressionEvaluator(String loggerName, PathEvaluator pathEvaluator) {
        Helper.checkNull(pathEvaluator, "pathEvaluator");
        logger = Helper.getLogger(loggerName);
        this.pathEvaluator = pathEvaluator;
    }

    /**
     * <p>
     * Evaluates the given expression.
     * </p>
     *
     * @param sourceId the ID of the source XRE object.
     * @param receiver the Android receiver.
     * @param expression the expression to be evaluated.
     * @return the evaluation result.
     * @throws IllegalArgumentException if expression is <code>null</code>/empty, sourceId is not positive or receiver
     * is <code>null</code>.
     * @throws InvalidExpressionException if the given expression has invalid format.
     * @throws InvalidAndroidReceiverStateException if the provided receiver or some XRE object was not properly
     * initialized.
     * @throws InvalidPathException if some XRE object path specified in the expression is invalid.
     */
    public double evaluateExpression(String expression, int sourceId, AndroidReceiver receiver)
        throws InvalidExpressionException, InvalidAndroidReceiverStateException, InvalidPathException {
        Date enterTime = new Date();
        String signature =
            getSignature("evaluateExpression(String expression, int sourceId, AndroidReceiver receiver)");

        // Log method entry
        /*Helper.logEntrance(logger, signature, new String[]{"expression", "sourceId", "receiver"}, new Object[]{
            expression, sourceId, receiver});*/

        try {
            Helper.checkNullOrEmpty(expression, "expression");
            Helper.checkPositive(sourceId, "sourceId");
            Helper.checkNull(receiver, "receiver");

            if (!expression.startsWith(BRACE_L) || !expression.endsWith(BRACE_R)) {
                throw new InvalidExpressionException("The expression should start with '{' and end with '}'.");
            }
            // Remove leading "{" and trailing "}" characters
            expression = expression.substring(1, expression.length() - 1).trim();

            // Get mapping from ID to XRE object
            Map<Integer, XREObject> elementsXREIdToObject = Helper.getElementsXREIdToObject(receiver);

            // Evaluate the expression
            double result = evaluateExpression(expression, sourceId, receiver, elementsXREIdToObject);

            // Log method exit
            // Helper.logExit(logger, signature, new Object[]{result}, enterTime);

            return result;
        } catch (IllegalArgumentException e) {
            // Log exception
            throw Helper.logException(logger, signature, e, "IllegalArgumentException is thrown.");
        } catch (InvalidExpressionException e) {
            // Log exception
            throw Helper.logException(logger, signature, e,
                "InvalidExpressionException is thrown evaluating the expression.");
        } catch (InvalidAndroidReceiverStateException e) {
            // Log exception
            throw Helper.logException(logger, signature, e,
                "InvalidAndroidReceiverStateException is thrown evaluating the expression.");
        } catch (InvalidPathException e) {
            // Log exception
            throw Helper
                .logException(logger, signature, e, "InvalidPathException is thrown evaluating the expression.");
        }
    }

    /**
     * <p>
     * Evaluates the given expression.
     * </p>
     *
     * @param expression the expression.
     * @param sourceId the ID of the source XRE object.
     * @param receiver the Android receiver.
     * @param elementsXREIdToObject the elements XRE id to object map.
     * @return the evaluation result.
     * @throws InvalidExpressionException if the given expression has invalid format.
     * @throws InvalidAndroidReceiverStateException if the provided receiver or some XRE object was not properly
     * initialized.
     * @throws InvalidPathException if some XRE object path specified in the expression is invalid.
     */
    private double evaluateExpression(String expression, int sourceId, AndroidReceiver receiver,
        Map<Integer, XREObject> elementsXREIdToObject) throws InvalidExpressionException,
        InvalidAndroidReceiverStateException, InvalidPathException {
        // Split expression
        String[] expressionParts = expression.split(SPACE);
        int expressionPartsLen = expressionParts.length;

        for (int i = 0; i < expressionPartsLen; i++) {
            expressionParts[i] = expressionParts[i].trim();

            checkExpression(expressionParts[i].length() == 0, "The expression cannot contain empty part.");
        }

        // Create a list for evaluation stack containing operands
        Stack<Double> evaluationStack = new Stack<Double>();
        int index = 0;
        while (index < expressionPartsLen) {
            String curPart = expressionParts[index];

            Double value = null;

            if (curPart.length() == 1 && OPERATORS.contains(curPart)) {
                if (evaluationStack.size() < 2) {
                    throw new InvalidExpressionException("Missing operand(s) for the operator '" + curPart + "'.");
                }

                // Extract the second operand from the evaluation stack
                double operand2 = evaluationStack.pop();
                // Extract the first operand from the evaluation stack
                double operand1 = evaluationStack.pop();

                value = calculate(curPart.charAt(0), operand1, operand2);
            } else {
                // Parse the current part
                value = parseValue(curPart, sourceId, receiver, elementsXREIdToObject);
            }

            // Put the value to the evaluation stack
            evaluationStack.push(value);

            ++index;
        }
        if (evaluationStack.size() != 1) {
            throw new InvalidExpressionException("The expression '" + expression
                + "' has invalid format (too many values).");
        }

        return evaluationStack.pop();
    }

    /**
     * Parses value from the expression part.
     * <p>
     * AndroidAssembly3 added support for method execution.
     *
     * @param curPart the expression part.
     * @param sourceId the ID of the source XRE object.
     * @param receiver the Android receiver.
     * @param elementsXREIdToObject the elements XRE id to object map.
     * @return the result.
     * @throws InvalidExpressionException if the given expression has invalid format.
     * @throws InvalidAndroidReceiverStateException if the provided receiver or some XRE object was not properly
     * initialized.
     * @throws InvalidPathException if some XRE object path specified in the expression is invalid.
     */
    private Double parseValue(String curPart, int sourceId, AndroidReceiver receiver,
        Map<Integer, XREObject> elementsXREIdToObject) throws InvalidExpressionException,
        InvalidAndroidReceiverStateException, InvalidPathException {
        Double result;
        try {
            // Parse the current part as a double
            result = Double.parseDouble(curPart);
        } catch (NumberFormatException e) {
            // Cannot be parsed as double

            if (!curPart.contains(Helper.DOT)) {
                throw new InvalidExpressionException("The string '" + curPart + "' cannot be parsed as a double.", e);
            }

            // Get the last separator
            int pos = curPart.lastIndexOf(Helper.DOT);
            checkExpression(pos == 0, "The property name cannot be empty.");

            // Extract object property name:
            String propertyName = curPart.substring(pos + 1);

            // Extract object DOM path
            String objectPath = curPart.substring(0, pos);

            // check if there is method execution
            String methodName = null;
            List<Object> params = new ArrayList<Object>();
            // check if last part is not function call
            if (objectPath.endsWith(")")) {
                // get the last separator
                pos = objectPath.lastIndexOf(Helper.DOT);
                checkExpression(pos == 0, "The method call cannot be empty in '" + objectPath + "'.");
                checkExpression(pos == -1, "The dot must be present before method call in '" + objectPath + "'.");

                // extract the method call
                String methodCall = objectPath.substring(pos + 1);

                // update object DOM path
                objectPath = objectPath.substring(0, pos);

                // parse method call
                pos = methodCall.indexOf('(');
                checkExpression(pos == 0, "The method name cannot be empty in '" + objectPath + "'.");
                checkExpression(pos == -1, "The '(' must be present before method name in '" + objectPath + "'.");
                methodName = methodCall.substring(0, pos);

                // process parameters
                String paramsString = methodCall.substring(pos + 1, methodCall.length() - 1);
                String[] paramStrings = paramsString.split(",\\s*");
                for (String paramString : paramStrings) {
                    try {
                        params.add(Integer.parseInt(paramString));
                    } catch (NumberFormatException e1) {
                        try {
                            params.add(Double.parseDouble(paramString));
                        } catch (NumberFormatException e2) {
                            throw new InvalidExpressionException("The method parameter string '" + paramString
                                + "' cannot be parsed to double or int.", e);
                        }
                    }
                }
            }

            checkExpression(objectPath.trim().length() == 0, "The object path cannot be empty.");

            // Get object ID by its DOM path
            int objectId = pathEvaluator.evaluatePath(objectPath, sourceId, receiver, true);

            // Get XRE object by its ID from the map
            Object object = elementsXREIdToObject.get(objectId);
            checkExpression(object == null, "The XRE object with id '" + objectId + "' cannot be found.");

            // AndroidAssembly3 added support for method execution
            // call method to get result
            if (methodName != null) {
                object = getMethodResult(object, methodName, params.toArray());
            }

            // Call the getter to get the property value
            result = getPropertyValue(object, propertyName);
        }

        return result;
    }

    /**
     * <p>
     * Gets the property value of the object by name.
     * </p>
     *
     * @param obj the object.
     * @param propertyName the property name.
     * @return the property value.
     * @throws InvalidExpressionException if any error occurs.
     */
    private static Double getPropertyValue(Object obj, String propertyName) throws InvalidExpressionException {
        String getterName = GETTER_PREFIX + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);

        try {
            // Call the getter to get the property value
            Number result = (Number) obj.getClass().getMethod(getterName).invoke(obj);

            if (result == null) {
                throw new InvalidExpressionException("The property value cannot be null.");
            }

            return result.doubleValue();
        } catch (NoSuchMethodException e) {
            throw new InvalidExpressionException("The method '" + getterName + "' cannot be found.", e);
        } catch (IllegalAccessException e) {
            throw new InvalidExpressionException("Failed to invoke the method.", e);
        } catch (InvocationTargetException e) {
            throw new InvalidExpressionException("Failed to invoke the method.", e);
        } catch (SecurityException e) {
            throw new InvalidExpressionException("A security error occurred.", e);
        } catch (ClassCastException e) {
            throw new InvalidExpressionException("The result is not a number value.", e);
        }
    }

    /**
     * Gets the method result.
     * <p>
     * AndroidAssembly3 new method
     *
     * @param obj the object.
     * @param methodName the name of the method.
     * @param params the method params
     * @return the property value.
     * @throws InvalidExpressionException if any error occurs.
     */
    private static Object getMethodResult(Object obj, String methodName, Object[] params)
        throws InvalidExpressionException {

        try {
            // parse parameter classes
            Class<?>[] paramsClasses = new Class[params.length];
            for (int i = 0; i < params.length; i++) {
                paramsClasses[i] = params[i] == null ? Object.class : params[i].getClass();
                if (Integer.class.equals(paramsClasses[i])) {
                    paramsClasses[i] = int.class;
                } else if (Double.class.equals(paramsClasses[i])) {
                    paramsClasses[i] = double.class;
                }
            }

            // call the method to get the property value
            Object result = obj.getClass().getMethod(methodName, paramsClasses).invoke(obj, params);

            if (result == null) {
                throw new InvalidExpressionException("The property value cannot be null.");
            }

            return result;
        } catch (NoSuchMethodException e) {
            throw new InvalidExpressionException("The method '" + methodName + "' cannot be found at class '"
                + obj.getClass().getName() + "'.", e);
        } catch (IllegalAccessException e) {
            throw new InvalidExpressionException("Failed to invoke the method.", e);
        } catch (InvocationTargetException e) {
            throw new InvalidExpressionException("Failed to invoke the method.", e);
        } catch (SecurityException e) {
            throw new InvalidExpressionException("A security error occurred.", e);
        } catch (ClassCastException e) {
            throw new InvalidExpressionException("The result is not a number value.", e);
        }
    }

    /**
     * <p>
     * Calculates the result of expression <code>operand1 op operand2</code>.
     * </p>
     *
     * @param op the operator.
     * @param operand1 the first operand.
     * @param operand2 the second operand.
     * @return the result.
     */
    private static double calculate(char op, double operand1, double operand2) {
        double result;

        switch (op) {
        case OP_ADD:
            // '+'
            result = operand1 + operand2;
            break;
        case OP_SUB:
            // '-'
            result = operand1 - operand2;
            break;
        case OP_MUL:
            // '*'
            result = operand1 * operand2;
            break;
        case OP_DIV:
            // '/'
            result = operand1 / operand2;
            break;
        case OP_MOD:
            // '%'
            result = operand1 % operand2;
            break;
        default:
            // '^'
            result = Math.pow(operand1, operand2);
            break;
        }
        return result;
    }

    /**
     * <p>
     * Checks the expression.
     * </p>
     *
     * @param invalidExpression the flag indicates whether the expression is valid.
     * @param message the error message.
     * @throws InvalidExpressionException if invalidExpression is <code>true</code>.
     */
    private static void checkExpression(boolean invalidExpression, String message) throws InvalidExpressionException {
        if (invalidExpression) {
            throw new InvalidExpressionException(message);
        }
    }

    /**
     * <p>
     * Gets the signature for given method for logging.
     * </p>
     *
     * @param method the method name.
     * @return the signature for given method.
     */
    private static String getSignature(String method) {
        return CLASS_NAME + Helper.DOT + method;
    }
}
